use crate::bpf_rs::bpf_rs;
use libbpf_rs::skel::{OpenSkel, SkelBuilder};
use libbpf_rs::{MapCore, MapFlags};
use std::mem::MaybeUninit;
use std::os::fd::AsFd;

mod bpf_srv6_router {
    include!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/src/bpf/srv6_router.skel.rs"
    ));
}

#[derive(Clone, Debug)]
pub struct KeyValue<K, V> {
    pub key: K,
    pub value: V,
}

pub type Ipv4RouteKeyValue = KeyValue<bpf_rs::Ipv4LpmKey, bpf_rs::Srv6Route>;
pub type Ipv6RouteKeyValue = KeyValue<bpf_rs::Ipv6LpmKey, bpf_rs::Srv6Route>;
pub type LocalSidConfigKeyValue = KeyValue<bpf_rs::Ipv6Addr8, bpf_rs::LocalSrv6SidConfig>;

#[derive(Clone, Debug)]
pub struct Config {
    pub local_sid_configs: Vec<LocalSidConfigKeyValue>,
    pub ipv4_routes: Vec<Ipv4RouteKeyValue>,
    pub ipv6_routes: Vec<Ipv6RouteKeyValue>,
}

enum Action {
    Attach,
    DetachAndDestroy,
}

#[derive(Default)]
struct Options {
    config: Option<Config>,
}

pub struct Router {
    iface: String,
    device_type: u32,
}

impl Router {
    pub fn new(iface: String, device_type: bpf_rs::DeviceType) -> Self {
        Self { iface, device_type }
    }

    pub fn iface(&self) -> &str {
        self.iface.as_str()
    }

    pub fn device_type(&self) -> u32 {
        self.device_type
    }

    fn apply(&self, action: Action, options: Options) -> Result<(), anyhow::Error> {
        let mut open_object = MaybeUninit::uninit();
        let builder = bpf_srv6_router::Srv6RouterSkelBuilder::default();
        let open = builder.open(&mut open_object).map_err(anyhow::Error::new)?;
        let skel = open.load().map_err(anyhow::Error::new)?;

        if let Some(config) = options.config {
            self.skel_set_config(&skel, &config)?;
        }

        let ifindex =
            nix::net::if_::if_nametoindex(self.iface.as_str()).map_err(anyhow::Error::new)?;
        let mut tc_builder = libbpf_rs::TcHookBuilder::new(skel.progs.wanmesh_srv6_router.as_fd());
        tc_builder
            .ifindex(ifindex as i32)
            .replace(true)
            .handle(1)
            .priority(1);

        let mut tc_hook = tc_builder.hook(libbpf_rs::TC_INGRESS);
        tc_hook.create().map_err(anyhow::Error::new)?;

        match action {
            Action::Attach => {
                tc_hook.attach().map_err(anyhow::Error::new)?;
            }
            Action::DetachAndDestroy => {
                tc_hook.detach().map_err(anyhow::Error::new)?;
                tc_hook.destroy().map_err(anyhow::Error::new)?;
            }
        }
        Ok(())
    }

    pub fn attach(&self, config: Config) -> Result<(), anyhow::Error> {
        self.apply(
            Action::Attach,
            Options {
                config: Some(config),
            },
        )
    }

    pub fn detach_and_destroy(&self) -> Result<(), anyhow::Error> {
        self.apply(Action::DetachAndDestroy, Default::default())
    }

    fn skel_set_config<'a>(
        &self,
        skel: &bpf_srv6_router::Srv6RouterSkel<'a>,
        config: &Config,
    ) -> Result<(), anyhow::Error> {
        skel.maps
            .config_device_type
            .update(
                bytemuck::bytes_of(&0u32),
                bytemuck::bytes_of(&self.device_type),
                MapFlags::ANY,
            )
            .map_err(anyhow::Error::new)?;

        for (i, kv) in config.local_sid_configs.iter().enumerate() {
            if i == bpf_rs::SRV6_SID_CONFIGS_MAX_LENGTH {
                break;
            }

            skel.maps
                .config_local_sid_configs
                .update(
                    bytemuck::bytes_of(&kv.key),
                    bytemuck::bytes_of(&kv.value),
                    MapFlags::ANY,
                )
                .map_err(anyhow::Error::new)?;
        }

        for (i, kv) in config.ipv4_routes.iter().enumerate() {
            if i == bpf_rs::LOCAL_ROUTES_MAX_LENGTH {
                break;
            }

            skel.maps
                .config_ipv4_routes
                .update(
                    bytemuck::bytes_of(&kv.key),
                    bytemuck::bytes_of(&kv.value),
                    MapFlags::ANY,
                )
                .map_err(anyhow::Error::new)?;
        }

        for (i, kv) in config.ipv6_routes.iter().enumerate() {
            if i == bpf_rs::LOCAL_ROUTES_MAX_LENGTH {
                break;
            }

            skel.maps
                .config_ipv6_routes
                .update(
                    bytemuck::bytes_of(&kv.key),
                    bytemuck::bytes_of(&kv.value),
                    MapFlags::ANY,
                )
                .map_err(anyhow::Error::new)?;
        }

        Ok(())
    }
}
